# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
import decimal
import json
import math

class GestureLog(object):
  """ Represents the gestures in an activity log.

  The gesture log is a representation of an activity log as it is generated
  by the replay tool or 'tpcontrol log'.
  It converts all gestures into a list of events using the classes below.
  To allow easier processing all events that belong together are merged into
  gestures. For example all scroll events from one scroll motion on the touchpad
  are merged to create a single scroll gesture.
  - self.events will contain the list of events
  - self.gestures the list of gestures.
  """
  def __init__(self, log):
    decimal.setcontext(decimal.Context(prec=8))
    self.raw = json.loads(log, parse_float=decimal.Decimal)
    raw_events = filter(lambda e: e['type'] == 'gesture', self.raw['entries'])
    self.events = self._ParseRawEvents(raw_events)
    self.gestures = self._MergeGestures(self.events)

    self.properties = self.raw['properties']
    self.hwstates = filter(lambda e: e['type'] == 'hardwareState',
                           self.raw['entries'])
    self.hwproperties = self.raw['hardwareProperties']

  def _ParseRawEvents(self, gesture_list):
    events = []

    for gesture in gesture_list:
      start_time = gesture['startTime']
      end_time = gesture['endTime']

      if gesture['gestureType'] == 'move':
        events.append(MotionGesture(gesture['dx'], gesture['dy'], start_time,
                                    end_time))

      elif gesture['gestureType'] == 'buttonsChange':
        if gesture['down'] != 0:
          button = gesture['down']
          events.append(ButtonDownGesture(button, start_time, end_time))
        if gesture['up'] != 0:
          button = gesture['up']
          events.append(ButtonUpGesture(button, start_time, end_time))

      elif gesture['gestureType'] == 'scroll':
        events.append(ScrollGesture(gesture['dx'], gesture['dy'], start_time,
                                    end_time))

      elif gesture['gestureType'] == 'pinch':
        events.append(PinchGesture(gesture['dz'], start_time, end_time))

      elif gesture['gestureType'] == 'swipe':
        events.append(SwipeGesture(gesture['dx'], gesture['dy'], start_time,
                                   end_time))

      elif gesture['gestureType'] == 'swipeLift':
        events.append(SwipeLiftGesture(start_time, end_time))

      elif gesture['gestureType'] == 'fling':
        if gesture['flingState'] == 1:
          events.append(FlingStopGesture(start_time, end_time))
        else:
          events.append(FlingGesture(gesture['vx'], gesture['vy'], start_time,
                                     end_time))
      elif gesture['gestureType'] == 'metrics':
        # ignore
        pass
      else:
        print 'Unknown gesture:', repr(gesture)

    return events

  def _MergeGestures(self, event_list):
    gestures = []
    last_event_of_type = {}

    for event in event_list:
      # merge motion and scroll events into gestures
      if (event.type == 'Motion' or event.type == 'Scroll' or
          event.type == 'Swipe' or event.type == 'Pinch'):
        if event.type not in last_event_of_type:
          last_event_of_type[event.type] = event
          gestures.append(event)
        else:
          if float(event.start - last_event_of_type[event.type].end) < 0.1:
            last_event_of_type[event.type].Append(event)
          else:
            last_event_of_type[event.type] = event
            gestures.append(event)
      else:
        if event.type == 'ButtonUp' or event.type == 'ButtonDown':
          last_event_of_type = {}
        gestures.append(event)
    return gestures


class AxisGesture(object):
  """ Generic gesture class to describe gestures with x/y or z axis. """

  def __init__(self, dx, dy, dz, start, end):
    """ Create a new instance describing a single axis event.

    To describe a list of events that form a gesture use the Append method.
    @param dx: movement in x coords
    @param dy: movement in y coords
    @param dz: movement in z coords
    @param start: start timestamp
    @param end: end timestamp
    """
    self.dx = math.fabs(dx)
    self.dy = math.fabs(dy)
    self.dz = math.fabs(dz)
    self.start = float(start)
    self.end = float(end)
    self.segments = []

    self.distance = math.sqrt(self.dx * self.dx + self.dy * self.dy +
                              self.dz * self.dz)
    self.segments.append(self.distance)

  def Append(self, motion):
    """ Append an motion event to build a gesture. """
    self.dx = self.dx + motion.dx
    self.dy = self.dy + motion.dy
    self.dz = self.dz + motion.dz
    self.distance = self.distance + motion.distance
    self.segments.append(motion.distance)
    self.end = motion.end

  def Distance(self):
    return self.distance

  def Speed(self):
    """ Average speed of motion in mm/s. """
    if self.end > self.start:
      return self.distance / (self.end - self.start)
    else:
      return float('+inf')

  def Roughness(self):
    """ Returns the roughness of this gesture.

    The roughness measures the variability in the movement events. A continuous
    stream of events with similar movement distances is considered to be smooth.
    Choppy movement with a high variation in movement distances on the other
    hand is considered as rough. i.e. a constant series of movement distances is
    considered perfectly smooth and will result in a roughness of 0.
    Whenever there are sudden changes, or very irregular movement distances
    the roughness will increase.
    """
    # Each event in the gesture resulted in a movement distance. These distances
    # are treated as a signal and high pass filtered. This results in a signal
    # containing only high frequency changes in the distance, i.e. the rough
    # parts.
    # The squared average of this signal is used as a measure for the roughness.

    # gaussian filter kernel with sigma=1:
    # The kernel is calculated using this formula (with s=sigma):
    # 1/sqrt(2*pi*s^2)*e^(-x^2/(2*s^2))
    # The array can be recalculated with modified sigma by entering the
    # following equation into http://www.wolframalpha.com/:
    # 1/sqrt(2*pi*s^2)*e^(-[-2, -1, 0, 1, 2]^2/(2*s^2))
    # (Replace s with the desired sigma value)
    gaussian = [0.053991, 0.241971, 0.398942, 0.241971, 0.053991]

    # normalize gaussian filter kernel
    gsum = sum(gaussian)
    gaussian = map(lambda g: g / gsum, gaussian)

    # add padding to the front/end of the distances
    segments = []
    segments.append(self.segments[0])
    segments.append(self.segments[0])
    segments.extend(self.segments)
    segments.append(self.segments[-1])
    segments.append(self.segments[-1])

    # low pass filter the distances
    segments_lp = []
    for i in range(2, len(segments) - 2):
      v = segments[i - 2] * gaussian[0]
      v = v + segments[i - 1] * gaussian[1]
      v = v + segments[i  ] * gaussian[2]
      v = v + segments[i + 1] * gaussian[3]
      v = v + segments[i + 2] * gaussian[4]
      segments_lp.append(v)

    # H_HP = 1 - H_LP
    segments_hp = []
    for i in range(0, len(self.segments)):
      segments_hp.append(self.segments[i] - segments_lp[i])
    # square signal and calculate squared average
    segments_hp_sq = map(lambda v:v * v, segments_hp)
    return math.sqrt(sum(segments_hp_sq) / len(segments_hp))

  def __str__(self):
    fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} z={4:.4g} r={5:.4g} s={6:.4g}'
    return fstr.format(self.__class__.type, self.distance, self.dx,
                       self.dy, self.dz, self.Roughness(), self.Speed())

  def __repr__(self):
    return str(self)

class MotionGesture(AxisGesture):
  """ The motion gesture is only using the X and Y axis. """
  type = 'Motion'

  def __init__(self, dx, dy, start, end):
    AxisGesture.__init__(self, dx, dy, 0, start, end)

  def __str__(self):
    fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} r={4:.4g} s={5:.4g}'
    return fstr.format(self.__class__.type, self.distance, self.dx,
                       self.dy, self.Roughness(), self.Speed())

class ScrollGesture(MotionGesture):
  """ The scroll gesture is functionally the same as the MotionGesture """
  type = 'Scroll'


class PinchGesture(AxisGesture):
  """ The pinch gesture is functionally the same as the MotionGesture.

  However only uses the dx variable to represent the zoom factor.
  """
  type = 'Pinch'

  def __init__(self, dz, start, end):
    AxisGesture.__init__(self, 0, 0, dz, start, end)

  def __str__(self):
    fstr = '{0} dz={1:.4g} r={2:.4g}'
    return fstr.format(self.__class__.type, self.dz, self.Roughness())


class SwipeGesture(MotionGesture):
  """ The swipe gesture is functionally the same as the MotionGesture """
  type = 'Swipe'


class FlingGesture(MotionGesture):
  """ The scroll gesture is functionally the same as the MotionGesture """
  type = 'Fling'


class FlingStopGesture(object):
  """ The FlingStop gesture only contains the start and end timestamp. """
  type = 'FlingStop'

  def __init__(self, start, end):
    self.start = start
    self.end = end

  def __str__(self):
    return self.__class__.type

  def __repr__(self):
    return str(self)


class SwipeLiftGesture(object):
  """ The SwipeLift gesture only contains the start and end timestamp. """
  type = 'SwipeLift'

  def __init__(self, start, end):
    self.start = start
    self.end = end

  def __str__(self):
    return self.__class__.type

  def __repr__(self):
    return str(self)


class AbstractButtonGesture(object):
  """ Abstract gesture for up and down button gestures.

  As both button down and up gestures are functionally identical it has
  been extracted to this class. The AbstractButtonGesture stores a button ID
  next to the start and end time of the gesture.
  """
  type = 'Undefined'

  def __init__(self, button, start, end):
    self.button = button
    self.start = start
    self.end = end

  def __str__(self):
    return self.__class__.type + '(' + str(self.button) + ')'

  def __repr__(self):
    return str(self)


class ButtonDownGesture(AbstractButtonGesture):
  """ Functionally the same as AbstractButtonGesture """
  type = 'ButtonDown'


class ButtonUpGesture(AbstractButtonGesture):
  """ Functionally the same as AbstractButtonGesture """
  type = 'ButtonUp'
